using System;
using System.Net;
using ch.ethz.ssh2;
using ch.ethz.ssh2.crypto;
using ch.ethz.ssh2.crypto.cipher;
using ch.ethz.ssh2.crypto.digest;
using ch.ethz.ssh2.log;
using ch.ethz.ssh2.packets;
using ch.ethz.ssh2.util;
using System.Net.Sockets;
using Org.BouncyCastle.Security;
using System.Threading;
namespace ch.ethz.ssh2.transport
{

    /*
    * Yes, the "standard" is a big mess. On one side, the say that arbitary channel
    * packets are allowed during kex exchange, on the other side we need to blindly
    * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that
    * the next packet is not a channel data packet? Yes, we could check if it is in
    * the KEX range. But the standard says nothing about this. The OpenSSH guys
    * block local "normal" traffic during KEX. That's fine - however, they assume
    * that the other side is doing the same. During re-key, if they receive traffic
    * other than KEX, they become horribly irritated and kill the connection. Since
    * we are very likely going to communicate with OpenSSH servers, we have to play
    * the same game - even though we could do better.
    * 
    * btw: having stdout and stderr on the same channel, with a shared window, is
    * also a VERY good idea... =(
    */

    /// <summary> TransportManager.
    /// 
    /// </summary>
    /// <author>  Christian Plattner
    /// </author>
    /// <version>  2.50, 03/15/10
    /// </version>
    public class TransportManager
    {
        virtual public int PacketOverheadEstimate
        {
            get
            {
                return tc.PacketOverheadEstimate;
            }

        }
        virtual public bool TcpNoDelay
        {
            set
            {
                client.NoDelay = value;
            }

        }
        virtual public int SoTimeout
        {
            set
            {
                client.ReceiveTimeout = value;
            }

        }
        //UPGRADE_NOTE: Exception 'java.lang.Throwable' was converted to 'System.Exception' which has different behavior. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1100'"
        virtual public System.Exception ReasonClosedCause
        {
            get
            {
                lock (connectionSemaphore)
                {
                    return reasonClosedCause;
                }
            }

        }
        virtual public byte[] SessionIdentifier
        {
            get
            {
                return km.sessionId;
            }

        }
        virtual public System.Collections.ArrayList ConnectionMonitors
        {
            set
            {
                lock (this)
                {
                    connectionMonitors = (System.Collections.ArrayList)value.Clone();
                }
            }

        }
        //UPGRADE_NOTE: Final was removed from the declaration of 'log '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
        //UPGRADE_NOTE: The initialization of  'log' was moved to static method 'ch.ethz.ssh2.transport.TransportManager'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1005'"
        private static readonly Logger log;

        //UPGRADE_NOTE: Field 'EnclosingInstance' was added to class 'HandlerEntry' to access its enclosing instance. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1019'"
        internal class HandlerEntry
        {
            public HandlerEntry(TransportManager enclosingInstance)
            {
                InitBlock(enclosingInstance);
            }
            private void InitBlock(TransportManager enclosingInstance)
            {
                this.enclosingInstance = enclosingInstance;
            }
            private TransportManager enclosingInstance;
            public TransportManager Enclosing_Instance
            {
                get
                {
                    return enclosingInstance;
                }

            }
            internal MessageHandler mh;
            internal int low;
            internal int high;
        }

        //UPGRADE_NOTE: Final was removed from the declaration of 'asynchronousQueue '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
        private System.Collections.ArrayList asynchronousQueue = System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList(10));
        private System.Threading.Thread asynchronousThread = null;

        internal System.String hostname;
        internal int port;
        //UPGRADE_NOTE: Final was removed from the declaration of 'sock '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
        //UPGRADE_ISSUE: Constructor 'java.net.Socket.Socket' was not converted. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1000_javanetSocketSocket'"
        internal System.Net.Sockets.TcpClient client = new TcpClient();

        internal System.Object connectionSemaphore = new System.Object();

        internal bool flagKexOngoing = false;
        internal bool connectionClosed = false;

        //UPGRADE_NOTE: Exception 'java.lang.Throwable' was converted to 'System.Exception' which has different behavior. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1100'"
        internal System.Exception reasonClosedCause = null;

        internal TransportConnection tc;
        internal KexManager km;

        internal System.Collections.ArrayList messageHandlers = System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList(10));

        internal Thread receiveThread;

        internal System.Collections.ArrayList connectionMonitors = System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList(10));
        internal bool monitorsWereInformed = false;

        /// <summary> There were reports that there are JDKs which use
        /// the resolver even though one supplies a dotted IP
        /// address in the Socket constructor. That is why we
        /// try to generate the InetAdress "by hand".
        /// 
        /// </summary>
        /// <param name="host">
        /// </param>
        /// <returns> the InetAddress
        /// </returns>
        /// <throws>  UnknownHostException </throws>
        private System.Net.IPAddress createInetAddress(System.String host)
        {
            /* Check if it is a dotted IP4 address */

            System.Net.IPAddress addr = parseIPv4Address(host);

            if (addr != null)
                return addr;

            //UPGRADE_TODO: The equivalent in .NET for method 'java.net.InetAddress.getByName' may return a different value. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1043'"
            return System.Net.Dns.Resolve(host).AddressList[0];
        }

        private System.Net.IPAddress parseIPv4Address(System.String host)
        {
            if (host == null)
                return null;
            return IPAddress.Parse(host);
            //System.String[] quad = Tokenizer.parseTokens(host, '.');

            //if ((quad == null) || (quad.Length != 4))
            //    return null;

            //byte[] addr = new byte[4];

            //for (int i = 0; i < 4; i++)
            //{
            //    int part = 0;

            //    if ((quad[i].Length == 0) || (quad[i].Length > 3))
            //        return null;

            //    for (int k = 0; k < quad[i].Length; k++)
            //    {
            //        char c = quad[i][k];

            //        /* No, Character.isDigit is not the same */
            //        if ((c < '0') || (c > '9'))
            //            return null;

            //        part = part * 10 + (c - '0');
            //    }

            //    if (part > 255)
            //        /* 300.1.2.3 is invalid =) */
            //        return null;

            //    addr[i] = (byte)part;
            //}
            //return InetAddress.getByAddress(host, addr);
        }

        public TransportManager(System.String host, int port)
        {
            this.hostname = host;
            this.port = port;
        }

        public virtual ConnectionInfo getConnectionInfo(int kexNumber)
        {
            return km.getOrWaitForConnectionInfo(kexNumber);
        }

        //UPGRADE_NOTE: Exception 'java.lang.Throwable' was converted to 'System.Exception' which has different behavior. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1100'"
        public virtual void close(System.Exception cause, bool useDisconnectPacket)
        {
            if (useDisconnectPacket == false)
            {
                /* OK, hard shutdown - do not aquire the semaphore,
                * perhaps somebody is inside (and waits until the remote
                * side is ready to accept new data). */

                try
                {
                    client.Close();
                }
                catch (System.IO.IOException ignore)
                {
                }

                /* OK, whoever tried to send data, should now agree that
                * there is no point in further waiting =)
                * It is safe now to aquire the semaphore.
                */
            }

            lock (connectionSemaphore)
            {
                if (connectionClosed == false)
                {
                    if (useDisconnectPacket == true)
                    {
                        try
                        {
                            //UPGRADE_TODO: The equivalent in .NET for method 'java.lang.Throwable.getMessage' may return a different value. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1043'"
                            byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.Message, "").Payload;
                            if (tc != null)
                                tc.sendMessage(msg);
                        }
                        catch (System.IO.IOException ignore)
                        {
                        }

                        try
                        {
                            client.Close();
                        }
                        catch (System.IO.IOException ignore)
                        {
                        }
                    }

                    connectionClosed = true;
                    reasonClosedCause = cause; /* may be null */
                }
                System.Threading.Monitor.PulseAll(connectionSemaphore);
            }

            /* No check if we need to inform the monitors */

            System.Collections.ArrayList monitors = null;

            lock (this)
            {
                /* Short term lock to protect "connectionMonitors"
                * and "monitorsWereInformed"
                * (they may be modified concurrently)
                */

                if (monitorsWereInformed == false)
                {
                    monitorsWereInformed = true;
                    monitors = (System.Collections.ArrayList)connectionMonitors.Clone();
                }
            }

            if (monitors != null)
            {
                for (int i = 0; i < monitors.Count; i++)
                {
                    try
                    {
                        ConnectionMonitor cmon = (ConnectionMonitor)monitors[i];
                        cmon.connectionLost(reasonClosedCause);
                    }
                    catch (System.Exception ignore)
                    {
                    }
                }
            }
        }

        private void establishConnection(ProxyData proxyData, int connectTimeout)
        {
            /* See the comment for createInetAddress() */

            if (proxyData == null)
            {
                System.Net.IPAddress addr = createInetAddress(hostname);
                client.Connect(addr, port);
                client.SendTimeout = connectTimeout;
                client.ReceiveTimeout = 0;
                return;
            }

            if (proxyData is HTTPProxyData)
            {
                HTTPProxyData pd = (HTTPProxyData)proxyData;

                /* At the moment, we only support HTTP proxies */

                System.Net.IPAddress addr = createInetAddress(pd.proxyHost);
                client.Connect(addr, pd.proxyPort);
                client.SendTimeout = connectTimeout;
                client.ReceiveTimeout = 0;

                /* OK, now tell the proxy where we actually want to connect to */

                System.Text.StringBuilder sb = new System.Text.StringBuilder();

                sb.Append("CONNECT ");
                sb.Append(hostname);
                sb.Append(':');
                sb.Append(port);
                sb.Append(" HTTP/1.0\r\n");

                if ((pd.proxyUser != null) && (pd.proxyPass != null))
                {
                    System.String credentials = pd.proxyUser + ":" + pd.proxyPass;
                    char[] encoded = Base64.encode(StringEncoder.GetBytes(credentials));
                    sb.Append("Proxy-Authorization: Basic ");
                    sb.Append(encoded);
                    sb.Append("\r\n");
                }

                if (pd.requestHeaderLines != null)
                {
                    for (int i = 0; i < pd.requestHeaderLines.Length; i++)
                    {
                        if (pd.requestHeaderLines[i] != null)
                        {
                            sb.Append(pd.requestHeaderLines[i]);
                            sb.Append("\r\n");
                        }
                    }
                }

                sb.Append("\r\n");

                System.IO.Stream output = client.GetStream();

                if (output is ch.ethz.ssh2.channel.ChannelOutputStream)
                    ((ch.ethz.ssh2.channel.ChannelOutputStream)output).write(StringEncoder.GetBytes(sb.ToString()));
                else
                {
                    byte[] temp_byteArray;
                    temp_byteArray = StringEncoder.GetBytes(sb.ToString());
                    output.Write(temp_byteArray, 0, temp_byteArray.Length);
                }
                output.Flush();

                /* Now parse the HTTP response */

                byte[] buffer = new byte[1024];
                System.IO.Stream input = client.GetStream();

                int len = ClientServerHello.readLineRN(input, buffer);

                System.String httpReponse = StringEncoder.GetString(buffer, 0, len);

                if (httpReponse.StartsWith("HTTP/") == false)
                    throw new System.IO.IOException("The proxy did not send back a valid HTTP response.");

                /* "HTTP/1.X XYZ X" => 14 characters minimum */

                if ((httpReponse.Length < 14) || (httpReponse[8] != ' ') || (httpReponse[12] != ' '))
                    throw new System.IO.IOException("The proxy did not send back a valid HTTP response.");

                int errorCode = 0;

                try
                {
                    errorCode = System.Int32.Parse(httpReponse.Substring(9, (12) - (9)));
                }
                catch (System.FormatException ignore)
                {
                    throw new System.IO.IOException("The proxy did not send back a valid HTTP response.");
                }

                if ((errorCode < 0) || (errorCode > 999))
                    throw new System.IO.IOException("The proxy did not send back a valid HTTP response.");

                if (errorCode != 200)
                {
                    throw new HTTPProxyException(httpReponse.Substring(13), errorCode);
                }

                /* OK, read until empty line */

                while (true)
                {
                    len = ClientServerHello.readLineRN(input, buffer);
                    if (len == 0)
                        break;
                }
                return;
            }

            throw new System.IO.IOException("Unsupported ProxyData");
        }

        public virtual void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex, int connectTimeout, SecureRandom rnd, ProxyData proxyData)
        {
            /* First, establish the TCP connection to the SSH-2 server */

            establishConnection(proxyData, connectTimeout);

            /* Parse the server line and say hello - important: this information is later needed for the
            * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object
            * for later use.
            */

            ClientServerHello csh = new ClientServerHello(client.GetStream(), client.GetStream());

            tc = new TransportConnection(client.GetStream(), client.GetStream(), rnd);

            km = new KexManager(this, csh, cwl, hostname, port, verifier, rnd);
            km.initiateKEX(cwl, dhgex);

            receiveThread = new Thread(new System.Threading.ThreadStart(delegate()
            {
                try
                {
                    receiveLoop();
                }
                catch (System.IO.IOException e)
                {
                    close(e, false);

                    if (ch.ethz.ssh2.transport.TransportManager.log.Enabled)
                    {
                        //UPGRADE_TODO: The equivalent in .NET for method 'java.lang.Throwable.getMessage' may return a different value. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1043'"
                        ch.ethz.ssh2.transport.TransportManager.log.log(10, "Receive thread: error in receiveLoop: " + e.Message);
                    }
                }

                if (ch.ethz.ssh2.transport.TransportManager.log.Enabled)
                    ch.ethz.ssh2.transport.TransportManager.log.log(50, "Receive thread: back from receiveLoop");

                /* Tell all handlers that it is time to say goodbye */

                if (km != null)
                {
                    try
                    {
                        km.handleMessage(null, 0);
                    }
                    catch (System.IO.IOException e)
                    {
                    }
                }

                for (int i = 0; i < messageHandlers.Count; i++)
                {
                    HandlerEntry he = (HandlerEntry)messageHandlers[i];
                    try
                    {
                        he.mh.handleMessage(null, 0);
                    }
                    catch (System.Exception ignore)
                    {
                    }
                }
            }));

            receiveThread.IsBackground = true;
            receiveThread.Start();
        }

        public virtual void registerMessageHandler(MessageHandler mh, int low, int high)
        {
            HandlerEntry he = new HandlerEntry(this);
            he.mh = mh;
            he.low = low;
            he.high = high;

            //lock (messageHandlers.SyncRoot)
            lock (messageHandlers)
            {
                messageHandlers.Add(he);
            }
        }

        public virtual void removeMessageHandler(MessageHandler mh, int low, int high)
        {
            //lock (messageHandlers.SyncRoot)
            lock (messageHandlers)
            {
                for (int i = 0; i < messageHandlers.Count; i++)
                {
                    HandlerEntry he = (HandlerEntry)messageHandlers[i];
                    if ((he.mh == mh) && (he.low == low) && (he.high == high))
                    {
                        messageHandlers.RemoveAt(i);
                        break;
                    }
                }
            }
        }

        public virtual void sendKexMessage(byte[] msg)
        {
            lock (connectionSemaphore)
            {
                if (connectionClosed)
                {
                    throw (System.IO.IOException)new System.IO.IOException("Sorry, this connection is closed.", reasonClosedCause);
                }

                flagKexOngoing = true;

                try
                {
                    tc.sendMessage(msg);
                }
                catch (System.IO.IOException e)
                {
                    close(e, false);
                    throw e;
                }
            }
        }

        public virtual void kexFinished()
        {
            lock (connectionSemaphore)
            {
                flagKexOngoing = false;
                System.Threading.Monitor.PulseAll(connectionSemaphore);
            }
        }

        public virtual void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex)
        {
            km.initiateKEX(cwl, dhgex);
        }

        public virtual void changeRecvCipher(BlockCipher bc, MAC mac)
        {
            tc.changeRecvCipher(bc, mac);
        }

        public virtual void changeSendCipher(BlockCipher bc, MAC mac)
        {
            tc.changeSendCipher(bc, mac);
        }

        public virtual void sendAsynchronousMessage(byte[] msg)
        {
            //lock (asynchronousQueue.SyncRoot)
            lock (asynchronousQueue)
            {
                asynchronousQueue.Add(msg);

                /* This limit should be flexible enough. We need this, otherwise the peer
                * can flood us with global requests (and other stuff where we have to reply
                * with an asynchronous message) and (if the server just sends data and does not
                * read what we send) this will probably put us in a low memory situation
                * (our send queue would grow and grow and...) */

                if (asynchronousQueue.Count > 100)
                    throw new System.IO.IOException("Error: the peer is not consuming our asynchronous replies.");

                /* Check if we have an asynchronous sending thread */

                if (asynchronousThread == null)
                {
                    asynchronousThread = new Thread(new ThreadStart(AsynchronousWork));
                    asynchronousThread.IsBackground = true;
                    asynchronousThread.Start();

                    /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */
                }
            }
        }

        private void AsynchronousWork()
        {
            while (true)
            {
                byte[] msg = null;

                //lock (asynchronousQueue.SyncRoot)
                lock (asynchronousQueue)
                {
                    if (asynchronousQueue.Count == 0)
                    {
                        /* After the queue is empty for about 2 seconds, stop this thread */

                        try
                        {
                            System.Threading.Monitor.Wait(asynchronousQueue, TimeSpan.FromMilliseconds(2000));
                        }
                        catch (System.Threading.ThreadInterruptedException e)
                        {
                            /* OKOK, if somebody interrupts us, then we may die earlier. */
                            Thread.CurrentThread.Interrupt();
                        }

                        if (asynchronousQueue.Count == 0)
                        {
                            asynchronousThread = null;
                            return;
                        }
                    }

                    System.Object tempObject;
                    tempObject = asynchronousQueue[0];
                    asynchronousQueue.RemoveAt(0);
                    msg = (byte[])tempObject;
                }

                /* The following invocation may throw an IOException.
                * There is no point in handling it - it simply means
                * that the connection has a problem and we should stop
                * sending asynchronously messages. We do not need to signal that
                * we have exited (asynchronousThread = null): further
                * messages in the queue cannot be sent by this or any
                * other thread.
                * Other threads will sooner or later (when receiving or
                * sending the next message) get the same IOException and
                * get to the same conclusion.
                */

                try
                {
                    sendMessage(msg);
                }
                catch (System.IO.IOException e)
                {
                    return;
                }
            }
        }

        public virtual void sendMessage(byte[] msg)
        {
            if (Thread.CurrentThread == receiveThread)
                throw new System.IO.IOException("Assertion error: sendMessage may never be invoked by the receiver thread!");

            lock (connectionSemaphore)
            {
                while (true)
                {
                    if (connectionClosed)
                    {
                        throw (System.IO.IOException)new System.IO.IOException("Sorry, this connection is closed.", reasonClosedCause);
                    }

                    if (flagKexOngoing == false)
                        break;

                    try
                    {
                        System.Threading.Monitor.Wait(connectionSemaphore);
                    }
                    catch (System.Threading.ThreadInterruptedException e)
                    {
                        Thread.CurrentThread.Interrupt();
                    }
                }

                try
                {
                    tc.sendMessage(msg);
                }
                catch (System.IO.IOException e)
                {
                    close(e, false);
                    throw e;
                }
            }
        }

        public virtual void receiveLoop()
        {
            byte[] msg = new byte[35000];

            while (true)
            {
                int msglen = tc.receiveMessage(msg, 0, msg.Length);

                int type = msg[0] & 0xff;

                if (type == Packets.SSH_MSG_IGNORE)
                    continue;

                if (type == Packets.SSH_MSG_DEBUG)
                {
                    if (log.Enabled)
                    {
                        TypesReader tr = new TypesReader(msg, 0, msglen);
                        tr.readByte();
                        tr.readBoolean();
                        System.Text.StringBuilder debugMessageBuffer = new System.Text.StringBuilder();
                        debugMessageBuffer.Append(tr.readString("UTF-8"));

                        for (int i = 0; i < debugMessageBuffer.Length; i++)
                        {
                            char c = debugMessageBuffer[i];

                            if ((c >= 32) && (c <= 126))
                                continue;
                            debugMessageBuffer[i] = '\uFFFD';
                        }

                        log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.ToString() + "'");
                    }
                    continue;
                }

                if (type == Packets.SSH_MSG_UNIMPLEMENTED)
                {
                    throw new System.IO.IOException("Peer sent UNIMPLEMENTED message, that should not happen.");
                }

                if (type == Packets.SSH_MSG_DISCONNECT)
                {
                    TypesReader tr = new TypesReader(msg, 0, msglen);
                    tr.readByte();
                    int reason_code = tr.readUINT32();
                    System.Text.StringBuilder reasonBuffer = new System.Text.StringBuilder();
                    reasonBuffer.Append(tr.readString("UTF-8"));

                    /*
                    * Do not get fooled by servers that send abnormal long error
                    * messages
                    */

                    if (reasonBuffer.Length > 255)
                    {
                        reasonBuffer.Length = 255;
                        reasonBuffer[254] = '.';
                        reasonBuffer[253] = '.';
                        reasonBuffer[252] = '.';
                    }

                    /*
                    * Also, check that the server did not send characters that may
                    * screw up the receiver -> restrict to reasonable US-ASCII
                    * subset -> "printable characters" (ASCII 32 - 126). Replace
                    * all others with 0xFFFD (UNICODE replacement character).
                    */

                    for (int i = 0; i < reasonBuffer.Length; i++)
                    {
                        char c = reasonBuffer[i];

                        if ((c >= 32) && (c <= 126))
                            continue;
                        reasonBuffer[i] = '\uFFFD';
                    }

                    throw new System.IO.IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): " + reasonBuffer.ToString());
                }

                /*
                * Is it a KEX Packet?
                */

                if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS) || ((type >= 30) && (type <= 49)))
                {
                    km.handleMessage(msg, msglen);
                    continue;
                }

                MessageHandler mh = null;

                for (int i = 0; i < messageHandlers.Count; i++)
                {
                    HandlerEntry he = (HandlerEntry)messageHandlers[i];
                    if ((he.low <= type) && (type <= he.high))
                    {
                        mh = he.mh;
                        break;
                    }
                }

                if (mh == null)
                    throw new System.IO.IOException("Unexpected SSH message (type " + type + ")");

                mh.handleMessage(msg, msglen);
            }
        }
        static TransportManager()
        {
            log = Logger.getLogger(typeof(TransportManager));
        }
    }
}